﻿using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Net;

namespace FileSharing
{
    #region Message
    public abstract class Message
    {
        protected Message()
        {
        }

        static public Message CreateMessage(BinaryReader stream)
        {
            ProtocolHandler.MessageCode messageType = (ProtocolHandler.MessageCode)stream.ReadUInt32();

            switch (messageType)
            {
                case ProtocolHandler.MessageCode.Hello:
                    return new MessageHello(stream);

                case ProtocolHandler.MessageCode.HelloReply:
                    return new MessageHelloReply(stream);

                case ProtocolHandler.MessageCode.Ping:
                    return new MessagePing(stream);

                case ProtocolHandler.MessageCode.Pong:
                    return new MessagePong(stream);

                case ProtocolHandler.MessageCode.NodeLookup:
                    return new MessageNodeLookup(stream);

                case ProtocolHandler.MessageCode.NodeLookupReply:
                    return new MessageNodeLookupReply(stream);

                case ProtocolHandler.MessageCode.Search:
                    return new MessageSearch(stream);

                case ProtocolHandler.MessageCode.SearchReply:
                    return new MessageSearchReply(stream);

                case ProtocolHandler.MessageCode.TransferHello:
                    return new MessageTransferHello(stream);

                default:
                    Log.Instance().PrintLine("Unrecongnized Message ID = " + messageType);
                    break;
            }

            throw new UnknownMessageException();
        }

        abstract public Message Answer(ProtocolHandler protocol, Connection conn);

        abstract public void Process(ProtocolHandler protocol, Connection conn);

        abstract public void Serialize(BinaryWriter stream);

        virtual public bool Processed(ProtocolHandler protocol)
        {
            return false;
        }
    }

    public class MessageHello : Message
    {
        int LocalPort;
        IPAddress RemoteAddress;
        string Name;

        public MessageHello(BinaryReader stream)
        {
            int port = stream.ReadInt32();
            byte[] address = stream.ReadBytes(4);
            char[] name = stream.ReadChars(16);

            LocalPort = port;
            Name = new string(name);
            RemoteAddress = new IPAddress(address);
        }

        public MessageHello(int port, IPAddress remote, string name)
        {
            LocalPort = port;
            Name = name;
            RemoteAddress = remote;
        }

        public override Message Answer(ProtocolHandler protocol, Connection conn)
        {
            return new MessageHelloReply(protocol.LocalName, conn.RemoteHost.Address);
        }

        public override void Process(ProtocolHandler protocol, Connection conn)
        {
            conn.ConnectVizinho.IP = new IPEndPoint(conn.RemoteHost.Address, LocalPort);
            conn.ConnectVizinho.Name = Name;

            Vizinho added = protocol.AddVizinho(conn.ConnectVizinho);

            if (added == null)
            {
                protocol.VizinhoRejected(conn);
                conn.Close();
            }
            else
            {
                conn.ConnectVizinho = added;
                added.ReadyForLookup = true;
                added.CurrentStatus = Vizinho.Status.Connected;

                protocol.LocalAddress.Address = RemoteAddress;

                protocol.VizinhoConnect(conn);
            }
        }

        public override void Serialize(BinaryWriter stream)
        {
            byte[] address = RemoteAddress.GetAddressBytes();
            char[] name = new char[16];

            Name.CopyTo(0, name, 0, Math.Min(Name.Length, 16));

            stream.Write((uint)ProtocolHandler.MessageCode.Hello);
            stream.Write(LocalPort);
            stream.Write(address, 0, address.Length);
            stream.Write(name, 0, 16);
        }
    }

    public class MessageHelloReply : Message
    {
        string Name;
        IPAddress RemoteAddress;

        public MessageHelloReply(BinaryReader stream)
        {
            char[] name = stream.ReadChars(16);
            byte[] address = stream.ReadBytes(4);

            Name = new string(name);
            RemoteAddress = new IPAddress(address);
        }

        public MessageHelloReply(string name, IPAddress client)
        {
            Name = name;
            RemoteAddress = client;
        }

        public override Message Answer(ProtocolHandler protocol, Connection conn)
        {
            conn.ConnectVizinho.LastNodeLookup = DateTime.Now;
            conn.ConnectVizinho.ReadyForLookup = false;

            return new MessageNodeLookup();
        }

        public override void Process(ProtocolHandler protocol, Connection conn)
        {
            Vizinho viz = conn.ConnectVizinho;

            viz.Name = Name;

            if (!protocol.LocalAddress.Address.Equals(RemoteAddress))
                protocol.LocalAddress.Address = RemoteAddress;

            conn.ConnectVizinho = protocol.AddVizinho(viz);
            
            if (conn.ConnectVizinho == null)
            {
                protocol.VizinhoRejected(conn);
                conn.Close();
            }
            else
            {
                conn.ConnectVizinho.CurrentStatus = Vizinho.Status.Connected;
                conn.ConnectVizinho.LastNodeLookup = DateTime.Now;
                conn.ConnectVizinho.ReadyForLookup = false;
                conn.ConnectVizinho.ReadyForPing = false;

                protocol.VizinhoConnect(conn);

                Log.Instance().PrintLine("Handshake succeessful to " + conn.ConnectVizinho.IP.ToString());
            }
        }
        public override void Serialize(BinaryWriter stream)
        {
            byte[] address = RemoteAddress.GetAddressBytes();
            char[] name = new char[16];

            Name.CopyTo(0, name, 0, Math.Min(Name.Length, 16));

            stream.Write((uint)ProtocolHandler.MessageCode.HelloReply);
            stream.Write(name, 0, 16);
            stream.Write(address, 0, address.Length);
        }
    }

    public class MessageNodeLookup : Message
    {
        public MessageNodeLookup(BinaryReader stream)
        {
        }

        public MessageNodeLookup()
        {
        }

        public override Message Answer(ProtocolHandler protocol, Connection conn)
        {
            return new MessageNodeLookupReply(protocol.ListaConhecidos);
        }

        public override void Process(ProtocolHandler protocol, Connection conn)
        {
        }

        public override void Serialize(BinaryWriter stream)
        {
            stream.Write((uint)ProtocolHandler.MessageCode.NodeLookup);
        }
    }

    public class MessageNodeLookupReply : Message
    {
        List<Vizinho> Vizinhos;

        public MessageNodeLookupReply(BinaryReader stream)
        {
            int count = stream.ReadInt32();

            Vizinhos = new List<Vizinho>(count);

            for (int i = 0; i < count; i++)
            {
                char[] name = stream.ReadChars(16);
                byte[] address = stream.ReadBytes(4);
                int port = stream.ReadInt32();

                Vizinho v = new Vizinho();
                v.Name = new string(name);
                v.IP = new IPEndPoint(new IPAddress(address), port);
                v.CurrentStatus = Vizinho.Status.Known;

                Vizinhos.Add(v);
            }
        }

        public MessageNodeLookupReply(List<Vizinho> lista)
        {
            Vizinhos = new List<Vizinho>();

            foreach (Vizinho v in lista)
            {
                if (v.CurrentStatus == Vizinho.Status.Active || v.CurrentStatus == Vizinho.Status.Connected)
                    Vizinhos.Add(v);
            }
        }

        public override Message Answer(ProtocolHandler protocol, Connection conn)
        {
            throw new NoAnswerException();
        }

        public override void Process(ProtocolHandler protocol, Connection conn)
        {
            Log.Instance().PrintLine("Received " + Vizinhos.Count.ToString() + " neighbors from " + conn.ConnectVizinho.IP.ToString());

            foreach (Vizinho v in Vizinhos)
            {
                protocol.AddVizinho(v);
            }

            conn.ConnectVizinho.ReadyForLookup = true;
        }

        public override void Serialize(BinaryWriter stream)
        {
            char[] name = new char[16];

            stream.Write((uint)ProtocolHandler.MessageCode.NodeLookupReply);

            stream.Write(Vizinhos.Count);

            foreach (Vizinho v in Vizinhos)
            {
                byte[] address = v.IP.Address.GetAddressBytes();

                v.Name.CopyTo(0, name, 0, v.Name.Length);
                stream.Write(name, 0, 16);
                stream.Write(address, 0, address.Length);
                stream.Write(v.IP.Port);
            }
        }
    }
    #endregion

    #region Broadcast Messages
    public abstract class BroadcastMessage : Message
    {
        public Guid ID { get; set; }
        public ushort TTL { get; set; }
        public ushort HoopsTaken { get; set; }
        public IPEndPoint From { get; set; }

        public BroadcastMessage(BinaryReader stream)
        {
            byte[] guid = stream.ReadBytes(16);
            TTL = stream.ReadUInt16();
            HoopsTaken = stream.ReadUInt16();

            byte[] fromAddress = stream.ReadBytes(4);
            int fromPort = stream.ReadInt32();

            From = new IPEndPoint(new IPAddress(fromAddress), fromPort);
            ID = new Guid(guid);
        }

        public BroadcastMessage(IPEndPoint source)
        {
            ID = Guid.NewGuid();
            TTL = 9;
            HoopsTaken = 0;

            From = source;
        }

        public override void Serialize(BinaryWriter stream)
        {
            byte[] guid = ID.ToByteArray();

            stream.Write(guid, 0, 16);
            stream.Write(TTL);
            stream.Write(HoopsTaken);

            byte[] FromAddress = From.Address.GetAddressBytes();

            stream.Write(FromAddress, 0, 4);
            stream.Write(From.Port);
        }

        public override Message Answer(ProtocolHandler protocol, Connection conn)
        {
            protocol.SendBroadcastMessage(this, conn.ConnectVizinho);
            return null;
        }

        public override bool Processed(ProtocolHandler protocol)
        {
            if (protocol.CheckMessage(this))
            {
                return true;
            }
            else
            {
                protocol.AddMessage(this);
                return false;
            }
        }
    }

    public abstract class RoutedMessage : BroadcastMessage
    {
        public IPEndPoint To { get; set; }

        public RoutedMessage(BinaryReader stream)
            : base(stream)
        {
            byte[] toAddress = stream.ReadBytes(4);
            int toPort = stream.ReadInt32();

            To = new IPEndPoint(new IPAddress(toAddress), toPort);
        }

        public RoutedMessage(IPEndPoint source, IPEndPoint dest)
            : base(source)
        {
            To = dest;
        }

        public override void Serialize(BinaryWriter stream)
        {
            base.Serialize(stream);

            byte[] ToAddress = To.Address.GetAddressBytes();

            stream.Write(ToAddress, 0, 4);
            stream.Write(To.Port);
        }

        public override Message Answer(ProtocolHandler protocol, Connection conn)
        {
            if (!protocol.LocalAddress.Equals(To))
            {
                protocol.SendRoutedMessage(this, conn.ConnectVizinho);
                throw new NoAnswerException();
            }

            return null;
        }
    }

    public class MessagePing : RoutedMessage
    {
        public MessagePing(BinaryReader stream)
            : base(stream)
        {
        }

        public MessagePing(IPEndPoint source, IPEndPoint dest)
            : base(source, dest)
        {
        }

        public override void Serialize(BinaryWriter stream)
        {
            stream.Write((uint)ProtocolHandler.MessageCode.Ping);

            base.Serialize(stream);
        }

        public override Message Answer(ProtocolHandler protocol, Connection conn)
        {
            base.Answer(protocol, conn);

            return new MessagePong(To, From);
        }

        public override void Process(ProtocolHandler protocol, Connection conn)
        {
        }
    }

    public class MessagePong : RoutedMessage
    {
        public MessagePong(BinaryReader stream)
            : base(stream)
        {
        }

        public MessagePong(IPEndPoint source, IPEndPoint dest)
            : base(source, dest)
        {
        }

        public override void Serialize(BinaryWriter stream)
        {
            stream.Write((uint)ProtocolHandler.MessageCode.Pong);

            base.Serialize(stream);
        }

        public override Message Answer(ProtocolHandler protocol, Connection conn)
        {
            base.Answer(protocol, conn);

            throw new NoAnswerException();
        }

        public override void Process(ProtocolHandler protocol, Connection conn)
        {
            if (protocol.LocalAddress.Equals(To))
            {
                protocol.VizinhoActive(From);
            }
        }
    }

    public class MessageSearch : BroadcastMessage
    {
        public String Query { get; set; }

        public MessageSearch(BinaryReader stream)
            : base(stream)
        {
            int querySize = stream.ReadInt32();
            char[] queryData = stream.ReadChars(querySize);

            Query = new String(queryData);
        }

        public MessageSearch(IPEndPoint source, String query)
            : base(source)
        {
            Query = query;
        }

        public override void Serialize(BinaryWriter stream)
        {
            stream.Write((uint)ProtocolHandler.MessageCode.Search);

            base.Serialize(stream);

            stream.Write(Query.Length);
            stream.Write(Query.ToCharArray(), 0, Query.Length);
        }

        public override Message Answer(ProtocolHandler protocol, Connection conn)
        {
            base.Answer(protocol, conn);

            LocalFile[] files = protocol.Search(Query);

            if (files.Length > 0)
            {
                RemoteFile[] remoteFiles = new RemoteFile[files.Length];

                for (int i = 0; i < remoteFiles.Length; i++)
                {
                    remoteFiles[i] = new RemoteFile(files[i]);
                }

                return new MessageSearchReply(protocol.LocalAddress, From, remoteFiles);
            }

            throw new NoAnswerException();
        }

        public override void Process(ProtocolHandler protocol, Connection conn)
        {
        }
    }

    public class MessageSearchReply : RoutedMessage
    {
        RemoteFile[] Files;

        public MessageSearchReply(BinaryReader stream)
            : base(stream)
        {
            int filesCount = stream.ReadInt32();

            Files = new RemoteFile[filesCount];

            for (int i = 0; i < filesCount; i++)
            {
                Files[i] = new RemoteFile(stream);
            }
        }

        public MessageSearchReply(IPEndPoint source, IPEndPoint dest, RemoteFile[] remoteFiles)
            : base(source, dest)
        {
            Files = remoteFiles;
        }

        public override void Serialize(BinaryWriter stream)
        {
            stream.Write((uint)ProtocolHandler.MessageCode.SearchReply);

            base.Serialize(stream);

            stream.Write(Files.Length);

            foreach (RemoteFile file in Files)
            {
                file.Serialize(stream);
            }
        }

        public override Message Answer(ProtocolHandler protocol, Connection conn)
        {
            base.Answer(protocol, conn);

            throw new NoAnswerException();
        }

        public override void Process(ProtocolHandler protocol, Connection conn)
        {
            if (To.Equals(protocol.LocalAddress))
            {
                foreach (RemoteFile f in Files)
                {
                    Vizinho viz = protocol.GetVizinho(this.From);

                    if (viz == null)
                    {
                        viz = new Vizinho();
                        viz.IP = this.From;

                        viz = protocol.AddVizinho(viz);
                    }

                    f.Owner = viz;
                }

                protocol.SearchMatchReply(Files);
            }
        }
    } 
    #endregion

    public class MessageTransferHello : Message
    {
        public IPEndPoint Host { get; set; }

        public MessageTransferHello(BinaryReader stream)
        {
            byte[] address = stream.ReadBytes(4);
            int port = stream.ReadInt32();

            Host = new IPEndPoint(new IPAddress(address), port);
        }

        public MessageTransferHello(IPEndPoint localHost)
        {
            Host = localHost;
        }

        public override Message Answer(ProtocolHandler protocol, Connection conn)
        {
            return null;
        }

        public override void Process(ProtocolHandler protocol, Connection conn)
        {
        }

        public override void Serialize(BinaryWriter stream)
        {
            stream.Write((uint)ProtocolHandler.MessageCode.TransferHello);

            byte[] address = Host.Address.GetAddressBytes();
            stream.Write(address, 0, 4);
            stream.Write(Host.Port);
        }
    }

    public abstract class TransferMessage
    {
        protected TransferMessage()
        {
        }

        static public TransferMessage CreateMessage(BinaryReader stream)
        {
            ProtocolHandler.MessageCode messageType = (ProtocolHandler.MessageCode)stream.ReadUInt32();

            switch (messageType)
            {
                case ProtocolHandler.MessageCode.TransferHelloReply:
                    return new MessageTransferHelloReply(stream);

                case ProtocolHandler.MessageCode.RequestTransfer:
                    return new MessageRequestTransfer(stream);

                case ProtocolHandler.MessageCode.RequestTransferReply:
                    return new MessageRequestTransferReply(stream);

                case ProtocolHandler.MessageCode.RequestPiece:
                    return new MessageRequestPiece(stream);

                case ProtocolHandler.MessageCode.PieceChunk:
                    return new MessagePieceChunk(stream);

                case ProtocolHandler.MessageCode.PieceEnd:
                    return new MessagePieceEnd(stream);

                case ProtocolHandler.MessageCode.TransferEnd:
                    return new MessageTransferEnd(stream);

                default:
                    Log.Instance().PrintLine("Unrecongnized Message ID = " + messageType);
                    break;
            }

            throw new UnknownMessageException();
        }

        abstract public TransferMessage Answer(TransferConnection conn);

        abstract public void Process(TransferConnection conn);

        abstract public int Serialize(BinaryWriter stream);
    }

    public class MessageTransferHelloReply : TransferMessage
    {
        public MessageTransferHelloReply(BinaryReader stream)
        {
        }

        public MessageTransferHelloReply()
        {
        }

        public override int Serialize(BinaryWriter stream)
        {
            stream.Write((uint)ProtocolHandler.MessageCode.TransferHelloReply);

            return 4;
        }

        public override TransferMessage Answer(TransferConnection conn)
        {
            return new MessageRequestTransfer(conn.DownloadTask.File);
        }

        public override void Process(TransferConnection conn)
        {
        }
    }

    public class MessageRequestTransfer : TransferMessage
    {
        RemoteFile File;

        public MessageRequestTransfer(BinaryReader stream)
        {
            File = new RemoteFile(stream);
        }

        public MessageRequestTransfer(RemoteFile f)
        {
            File = f;
        }

        public override int Serialize(BinaryWriter stream)
        {
            int bytes = 4;

            stream.Write((uint)ProtocolHandler.MessageCode.RequestTransfer);

            bytes = File.Serialize(stream);

            return bytes;
        }

        public override TransferMessage Answer(TransferConnection conn)
        {
            try
            {
                LocalFile local = conn.UploadTask.FindLocalFile(File);

                Log.Instance().PrintLine("Starting upload of " + local.Name + " to " + conn.ConnectVizinho.IP);

                return new MessageRequestTransferReply(local);
            }
            catch (FileNotFoundException e)
            {
                return new MessageTransferEnd(1);
            }
        }

        public override void Process(TransferConnection conn)
        {
        }
    }

    public class MessageRequestTransferReply : TransferMessage
    {
        byte[] AvaiblePieces;

        public MessageRequestTransferReply(BinaryReader stream)
        {
            int length = stream.ReadInt32();
            int size = stream.ReadInt32();
            byte[] data = stream.ReadBytes(size);

            AvaiblePieces = new byte[length];

            for (int i = 0, j = 0; i < length; i++)
            {
                AvaiblePieces[i] = (byte)((data[i / 8] & (1 << j)) > 0 ? 0xff : 0);

                j++;

                if (j == 8)
                    j = 0;
            }
        }

        public MessageRequestTransferReply(LocalFile file)
        {
            AvaiblePieces = file.AvaiblePieces();
        }

        public override int Serialize(BinaryWriter stream)
        {
            stream.Write((uint)ProtocolHandler.MessageCode.RequestTransferReply);

            int size = (int)Math.Ceiling((double)AvaiblePieces.Length / 8);
            byte[] data = new byte[size];

            for (int i = 0, j = 0; i < AvaiblePieces.Length; i++)
            {
                data[i / 8] |= (byte)((AvaiblePieces[i] == 0xFF ? 1 : 0) << j);

                j++;

                if (j == 8)
                    j = 0;
            }

            stream.Write(AvaiblePieces.Length);
            stream.Write(size);
            stream.Write(data, 0, size);

            return sizeof(uint) + sizeof(int) * 2 + size;
        }

        public override TransferMessage Answer(TransferConnection conn)
        {
            int sequence;

            if (conn.DownloadTask.NextPiece(AvaiblePieces, out sequence) == true)
            {
                conn.StartReceivePiece(sequence);
                return new MessageRequestPiece(sequence);
            }
            else
                return new MessageTransferEnd(0);
        }

        public override void Process(TransferConnection conn)
        {
            conn.AvaiblePieces = AvaiblePieces;
        }
    }

    public class MessageRequestPiece : TransferMessage
    {
        int SequenceNumber;

        public MessageRequestPiece(BinaryReader stream)
        {
            SequenceNumber = stream.ReadInt32();
        }

        public MessageRequestPiece(int sequence)
        {
            SequenceNumber = sequence;
        }

        public override int Serialize(BinaryWriter stream)
        {
            stream.Write((uint)ProtocolHandler.MessageCode.RequestPiece);

            stream.Write(SequenceNumber);

            return sizeof(int) * 2;
        }

        public override TransferMessage Answer(TransferConnection conn)
        {
            throw new NoAnswerException();
        }

        public override void Process(TransferConnection conn)
        {
            conn.StartSendPiece(SequenceNumber);
        }
    }

    public class MessagePieceChunk : TransferMessage
    {
        int SequenceNumber;
        byte[] Data;

        public MessagePieceChunk(BinaryReader stream)
        {
            SequenceNumber = stream.ReadInt32();

            int size = stream.ReadInt32();
            Data = stream.ReadBytes(size);
        }

        public MessagePieceChunk(int sequence, byte[] data)
        {
            SequenceNumber = sequence;
            Data = data;
        }

        public override int Serialize(BinaryWriter stream)
        {
            stream.Write((uint)ProtocolHandler.MessageCode.PieceChunk);

            stream.Write(SequenceNumber);
            stream.Write(Data.Length);
            stream.Write(Data, 0, Data.Length);

            return sizeof(int) * 3 + Data.Length;
        }

        public override TransferMessage Answer(TransferConnection conn)
        {
            throw new NoAnswerException();
        }

        public override void Process(TransferConnection conn)
        {
            conn.ReceivedChunk(SequenceNumber, Data);
        }
    }

    public class MessagePieceEnd : TransferMessage
    {
        byte[] Hash;

        public MessagePieceEnd(BinaryReader stream)
        {
            int size = stream.ReadInt32();
            Hash = stream.ReadBytes(size);
        }

        public MessagePieceEnd(byte[] hash)
        {
            Hash = hash;
        }

        public override int Serialize(BinaryWriter stream)
        {
            stream.Write((uint)ProtocolHandler.MessageCode.PieceEnd);

            stream.Write(Hash.Length);
            stream.Write(Hash, 0, Hash.Length);

            return sizeof(uint) * 2 + Hash.Length;
        }

        public override TransferMessage Answer(TransferConnection conn)
        {
            throw new NoAnswerException();
        }

        public override void Process(TransferConnection conn)
        {
            conn.ReceivedPiece(Hash);
        }
    }


    public class MessageTransferEnd : TransferMessage
    {
        int ErrorCode;

        public MessageTransferEnd(BinaryReader stream)
        {
        }

        public MessageTransferEnd(int error)
        {
            ErrorCode = error;
        }

        public override int Serialize(BinaryWriter stream)
        {
            stream.Write((uint)ProtocolHandler.MessageCode.TransferEnd);

            return sizeof(uint);
        }

        public override TransferMessage Answer(TransferConnection conn)
        {
            throw new NoAnswerException();
        }

        public override void Process(TransferConnection conn)
        {
            conn.TransferEnd();
        }
    }

    public class NoAnswerException : Exception
    {
    }

    public class UnknownMessageException : Exception
    {
    }
}
